use std::sync::{Arc, Mutex};

use axum::{
    extract::{Path, State},
    http::{header, StatusCode, Uri},
    response::{Html, IntoResponse},
    routing::{get, put},
    Json, Router,
};
use rust_embed::RustEmbed;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::net::TcpListener;

#[derive(RustEmbed)]
#[folder = "assets/"]
struct Assets;

#[derive(Debug, Serialize, Deserialize)]
pub struct Todo {
    pub id: String,
    pub title: String,
    pub is_done: bool,
}

type TodoList = Vec<Todo>;

type Store = Arc<Mutex<TodoList>>;

#[derive(Debug, Serialize, Deserialize)]
pub struct JsonResp<T> {
    pub code: i32,
    pub msg: String,
    pub data: T,
}
impl<T: Serialize + DeserializeOwned> JsonResp<T> {
    pub fn new(code: i32, msg: String, data: T) -> Self {
        Self { code, msg, data }
    }
    pub fn ok(data: T) -> Self {
        Self::new(0, "OK".to_string(), data)
    }
}

impl JsonResp<()> {
    pub fn err(msg: &str) -> Self {
        Self::new(-1, msg.to_string(), ())
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AddForm {
    pub title: String,
}

#[tokio::main]
async fn main() {
    let state: Store = Arc::new(Mutex::new(vec![]));

    let api_router = Router::new()
        .route("/", get(list).post(add))
        .route("/:id", put(mark_done));

    let app = Router::new()
        .nest("/api", api_router)
        .fallback(static_handler)
        .with_state(state);

    let listener = TcpListener::bind(&"127.0.0.1:56789").await.unwrap();

    axum::serve(listener, app).await.unwrap();
}

async fn static_handler(uri: Uri) -> impl IntoResponse {
    let path = uri.path().trim_start_matches("/");

    if path.is_empty() || path == "index.html" {
        return index_html().await.into_response();
    }

    println!("path: {path}");

    match Assets::get(path) {
        Some(content) => {
            let mime = mime_guess::from_path(path).first_or_octet_stream();
            ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
        }
        None => index_html().await.into_response(),
    }
}

async fn index_html() -> impl IntoResponse {
    if let Some(data) = Assets::get("index.html") {
        return Html(data.data).into_response();
    }
    not_found().await.into_response()
}

async fn not_found() -> impl IntoResponse {
    (StatusCode::NOT_FOUND, "404 Not Found")
}

async fn add(State(state): State<Store>, Json(frm): Json<AddForm>) -> Json<JsonResp<String>> {
    let id = xid::new().to_string();
    let m = Todo {
        id: id.clone(),
        title: frm.title,
        is_done: false,
    };
    let mut s = state.lock().unwrap();
    s.push(m);
    Json(JsonResp::ok(id))
}

async fn list(State(state): State<Store>) -> Json<JsonResp<TodoList>> {
    let s = state.lock().unwrap();
    let todo_list = s
        .iter()
        .map(|i| Todo {
            id: i.id.clone(),
            title: i.title.clone(),
            is_done: i.is_done,
        })
        .collect::<TodoList>();
    Json(JsonResp::ok(todo_list))
}

async fn mark_done(
    State(state): State<Store>,
    Path(id): Path<String>,
) -> Json<JsonResp<(String, u64)>> {
    let mut s = state.lock().unwrap();
    let item = s.iter_mut().find(|i| &i.id == &id).unwrap();
    item.is_done = true;
    Json(JsonResp::ok((id, 1)))
}
